﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using SlamCms.Common;

namespace SlamCms.Data
{
	public class QueryResult<T> : IEnumerable<T> where T : ContentItem
	{
		// TODO defer query execution until enumeration
		// TODO make the paginator deserialize only the needed page

		private bool _dirtyUsers = false;
		private bool _dirtyStatus = false;
		private bool _dirtyLists = false;

		private IDataReader _dataReader;
		private string _sql;
		private readonly DataContext _dataContext;
		private readonly QueryParameters _parameters;
		protected List<T> _results;

		private static Dictionary<string, object> _deserializers = new Dictionary<string, object>();

		internal QueryResult(DataContext dataContext, QueryParameters parameters)
		{
			_results = new List<T>();
			_dataContext = dataContext;
			_parameters = parameters;
			Execute();
		}

		public QueryParameters Parameters
		{
			get
			{
				return _parameters;
			}
		}

		public QueryStatistics Statistics { get; protected set; }

		protected void Execute()
		{
			ResetStatistics();

			var stopWatch = Stopwatch.StartNew();

			ExecuteQuery();

			stopWatch.Stop();
			Statistics.Total = stopWatch.Elapsed;
		}

		protected void ExecuteQuery()
		{
			GenerateQuery();

			_dataContext.Database.ExecuteReader(_sql, reader =>
			{
				_dataReader = reader;
				Statistics.QueryExecution = _dataContext.Database.LastCommandExecutionTime;
				DeserializeResults();
			});

			CheckDirtyCollections();
		}

		protected void GenerateQuery()
		{
			var slamQueryBuilder = new SqlQueryGenerator(_parameters);
			_sql = slamQueryBuilder.Generate();
			Statistics.QueryBuilding = slamQueryBuilder.QueryBuildElapsedTime;
		}

		protected void DeserializeResults()
		{
			var stopWatch = Stopwatch.StartNew();

			DeserializeContentItems();

			stopWatch.Stop();
			Statistics.Deserialization = stopWatch.Elapsed;
		}

		protected void DeserializeContentItems()
		{
			var reader = this._dataReader;
			var discriminatorIndex = reader.GetOrdinal(Constants.ContentTypeDiscriminatorFieldName);
			var contentItemIdIndex = reader.GetOrdinal("ContentItem.ContentItemId");
			var tagIdIndex = _parameters.IncludeTags ? reader.GetOrdinal("TagId") : 0;

			T contentItem = null;
			Tag tag;

			try
			{
				while (reader.Read())
				{
					// if discriminator is null then we cannot create the concrete type instance
					if (reader.IsDBNull(discriminatorIndex))
						continue;

					var contentItemId = reader.GetString(contentItemIdIndex);
					if (contentItem == null || contentItem.ContentItemId != contentItemId)
					{
						if (_parameters.Take > 0 && _results.Count >= _parameters.Take)
							return;

						contentItem = DeserializeContentItem(reader, discriminatorIndex);

						if (!_results.Any(ci => ci.ContentItemId == contentItem.ContentItemId))
							_results.Add(contentItem);
					}

					if (_parameters.IncludeTags && !reader.IsDBNull(tagIdIndex))
					{
						var tagId = reader.GetString(tagIdIndex);
						tag = _dataContext.Tags.Find(tagId);
						if (tag != null)
							contentItem.Tags.Add(tag);
					}
				}
			}
			catch (Exception ex)
			{
				throw new Exception("Error deserializing content items. See the inner exception for details.\r\n\r\n{0}".F(_sql), ex);
			}
		}

		protected T DeserializeContentItem(IDataReader reader, int discriminatorIndex)
		{
			var typeName = reader.GetString(discriminatorIndex);
			var type = typeof(ContentItem);

			if (typeName.IsNullOrEmpty())
				typeName = type.Name;
			else
			{
				type = _dataContext.Configuration.ContentTypes.GetTypeByName(typeName);

				if (type == null)
					throw new Exception("Cannot get deserializer for {0}. It's not a known content type.".F(typeName));
			}

			var deserializer = GetDeserializer<T>(typeName, type);

			var contentItem = deserializer(reader);

			LoadContentItemRelationships(contentItem);

			return contentItem;
		}

		protected void LoadContentItemRelationships(T contentItem)
		{
			if (contentItem.Tags == null)
				contentItem.Tags = new List<Tag>();

			User user;
			if (contentItem.ModifiedByUser == null)
			{
				user = _dataContext.Users.Find(contentItem.ModifiedByUserId);
				if (user != null)
					contentItem.ModifiedByUser = user;
				else
					_dirtyUsers = true;
			}

			if (contentItem.CreatedByUser == null)
			{
				user = _dataContext.Users.Find(contentItem.CreatedByUserId);
				if (user != null)
					contentItem.CreatedByUser = user;
				else
					_dirtyUsers = true;
			}

			if (contentItem.VerifiedByUser == null && contentItem.VerifiedByUserId.HasValue)
			{
				user = _dataContext.Users.Find(contentItem.VerifiedByUserId.Value);
				if (user != null)
					contentItem.VerifiedByUser = user;
				else
					_dirtyUsers = true;
			}

			if (contentItem.Status == null)
			{
				var status = _dataContext.Status.Find(contentItem.StatusID);
				if (status != null)
					contentItem.Status = status;
				else
					_dirtyStatus = true;
			}

			if (contentItem.List == null)
			{
				var list = _dataContext.Lists.Find(contentItem.ListId);
				if (list != null)
					contentItem.List = list;
				else
					_dirtyLists = true;
			}

			if (contentItem.ContentType == null)
			{
				var contentType = _dataContext.ContentTypes.Find(contentItem.ContentTypeId);
				if (contentType != null)
					contentItem.ContentType = contentType;
				else
					_dirtyLists = true;
			}
		}

		protected void CheckDirtyCollections()
		{
			var stopWatch = Stopwatch.StartNew();

			if (!_dirtyUsers && !_dirtyStatus && !_dirtyLists)
				return;

			if (_dirtyUsers)
				_dataContext.Cache.InvalidateUsers();

			if (_dirtyStatus || _dirtyLists)
				_dataContext.Cache.InvalidateCore();

			_dataContext.EnsureCache();

			LoadNullContentItemRelationships();

			stopWatch.Stop();
			this.Statistics.CheckDirtyCollections = stopWatch.Elapsed;
		}

		protected void LoadNullContentItemRelationships()
		{
			foreach (var contentItem in _results)
			{
				LoadContentItemRelationships(contentItem);
			}
		}

		protected void DeserializeTags()
		{
			var reader = _dataReader;

			reader.NextResult();

			var contentItemIdIndex = reader.GetOrdinal("ContentItemId");
			var tagIdIndex = reader.GetOrdinal("TagID");

			Tag tag;

			while (reader.Read())
			{
				var contentItemId = reader.GetString(contentItemIdIndex);
				var tagId = reader.GetString(tagIdIndex);

				var contentItem = this.FirstOrDefault(ci => ci.ContentItemId == contentItemId);

				if (contentItem != null)
				{
					if (contentItem.Tags == null)
						contentItem.Tags = new List<Tag>();

					tag = _dataContext.Tags.Find(tagId);
					if (tag != null)
						contentItem.Tags.Add(tag);
				}
			}
		}

		protected Func<IDataReader, U> GetDeserializer<U>(string typeName, Type type)
		{
			var deserializer = GetDeserializerFromCache<U>(typeName);
			if (deserializer == null)
			{
				deserializer = Deserializer.GetClassDeserializer<U>(_dataReader, type);
				SaveDeserializerToCache(typeName, deserializer);
			}

			return deserializer;
		}

		private static Func<IDataReader, U> GetDeserializerFromCache<U>(string key)
		{
			object deserializer;
			if (!_deserializers.TryGetValue(key, out deserializer))
				return null;

			return deserializer as Func<IDataReader, U>;
		}

		private void SaveDeserializerToCache(string key, object deserializer)
		{
			lock (_deserializers)
			{
				// check again just in case another thread already added it
				if (!_deserializers.ContainsKey(key))
					_deserializers[key] = deserializer;
			}
		}

		public IEnumerator<T> GetEnumerator()
		{
			return _results.GetEnumerator();
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return _results.GetEnumerator();
		}

		public QueryResult<T> Rank(Func<T, int> rankingFunction)
		{
			if (rankingFunction == null)
				throw new ArgumentNullException("rankingFunction");

			var newList = new Dictionary<int, T>();

			foreach (var contentItem in this._results)
			{
				var rank = rankingFunction(contentItem);
				newList.Add(rank, contentItem);
			}

			this._results = newList.OrderByDescending(x => x.Key).Select(x => x.Value).ToList();

			return this;
		}

		// PARALLEL IMPLEMENTATION (CANNOT USE IT BECAUSE IT'S 3.5)
		//public SlamQueryResult<T> Rank(Func<T, int> rankingFunction)
		//{
		//    if (rankingFunction == null)
		//        throw new ArgumentNullException("rankingFunction");

		//    var newList = new ConcurrentBag<KeyValuePair<int, T>>();

		//    foreach (var contentItem in this.results.AsParallel())
		//    {
		//        var rank = rankingFunction(contentItem);
		//        newList.Add(new KeyValuePair<int, T>(rank, contentItem));
		//    }

		//    this.results = newList.OrderByDescending(x => x.Key).Select(x => x.Value).ToList();

		//    return this;
		//}

		public QueryPagedResult<T> GetPage(int pageNumber, int pageSize = 10)
		{
			var pagedResults = new QueryPagedResult<T>(this, pageNumber, pageSize);
			return pagedResults;
		}

		protected void ResetStatistics()
		{
			Statistics = new QueryStatistics();
		}

		public class QueryStatistics
		{
			internal QueryStatistics()
			{
			}

			public TimeSpan QueryBuilding { get; internal set; }
			public TimeSpan QueryExecution { get; internal set; }
			public TimeSpan Deserialization { get; internal set; }
			public TimeSpan CheckDirtyCollections { get; internal set; }
			public TimeSpan ResultsRanking { get; internal set; }
			public TimeSpan Total { get; internal set; }

			public override string ToString()
			{
				var sb = new StringBuilder();
				sb.AppendLine(AddDuration("Query Building", QueryBuilding));
				sb.AppendLine(AddDuration("Query Execution", QueryExecution));
				sb.AppendLine(AddDuration("Deserialization", Deserialization));
				sb.AppendLine(AddDuration("Check Dirty Collections", CheckDirtyCollections));
				sb.AppendLine(AddDuration("Results Ranking", ResultsRanking));
				sb.AppendLine("Total: {0}".F(Total));
				return sb.ToString();
			}

			protected string AddDuration(string name, TimeSpan duration)
			{
				return "{0}: {1} - {2}%".F(name, duration, GetPercent(duration));
			}

			protected string GetPercent(TimeSpan duration)
			{
				return (duration.TotalMilliseconds / Total.TotalMilliseconds * 100d).ToString("0.00");
			}
		}
	}
}